<template>
  <el-upload
    action="/"
    :data="data"
    :accept="accept"
    :limit="limit"
    :multiple="multiple"
    :disabled="disabled"
    :show-file-list="false"
    :http-request="handleHttpRequest"
    ref="upload"
  >
    <slot>
      <div v-if="fileName && !multiple">
        <span
          class="f-14"
          v-if="fileName"
        >{{ fileName }}</span>
        <i
          class="el-icon-circle-close m-l-mini"
          @click.stop="clearFile"
        />
      </div>
      <el-button
        v-else
        size="small"
        type="primary"
        plain
        icon="el-icon-upload2"
      >
        上传文件
      </el-button>
    </slot>
  </el-upload>
</template>

<script>
import { mapState } from 'vuex'
import { getMd5, DEFAULT_SIZE, MAX_REQ_COUNT, RETRY } from './utils'
import { getTaskInfo, initTask, getPreSignUrl, mergeFile } from './api'
import Queue from 'promise-queue-plus'
import axios from 'axios'
import dayjs from 'dayjs'
const duration = require('dayjs/plugin/duration')
dayjs.extend(duration)

function convertSeconds (seconds) {
  const s = parseInt(seconds)
  return dayjs.duration(s, 's').format('HH:mm:ss')
}

export default {
  name: 'SegementUpload',
  props: {
    accept: {
      type: String,
      default: ''
    },
    data: {
      type: Object,
      default: null
    },
    fileType: {
      type: String,
      default: 'video',
      validator (v) {
        return 'image,video,720,orthoimage,dem,dsm,3d,vector,laser,pdf'.split(',').includes(v)
      }
    },
    multiple: {
      type: Boolean,
      default: true
    },
    extraParams: {
      type: Object,
      default: () => ({})
    }
  },
  data () {
    return {
      // 文件上传分块任务的队列（用于移除文件时，停止该文件的上传队列） key：fileUid value： queue object
      fileUploadChunkQueue: {},
      fileName: '',
      // 正在上传文件列表
      currentFiles: [],
      // 最大同时上传的文件个数
      maxFileCount: window._maxFileCount || 2,
      // 待上传文件列表
      uploadFileList: [],
      // 上传发生错误的文件映射
      errorFiles: {},
      // 文件取消状态
      fileCancelState: {}
    }
  },
  computed: {
    ...mapState('settings', ['theme']),
    disabled () {
      return this.multiple ? false : !!this.fileName
    },
    limit () {
      return this.multiple ? null : 1
    },
    isMultiFileMode () {
      return this.theme.hasFileManager && this.multiple
    }
  },
  methods: {
    /**
     * 获取一个上传任务，没有则初始化一个
     */
    async initTaskAndSplitFile (file) {
      let task
      const md5 = await getMd5(file)
      if (this.fileCancelState[file.uid]) return
      const data = await getTaskInfo(md5)
      if (data.code) {
        const taskOption = {
          identifier: md5,
          fileName: file.name,
          totalSize: file.size,
          chunkSize: DEFAULT_SIZE
        }
        task = await initTask(taskOption)
      } else {
        task = data
      }
      return task
    },
    /**
     * 上传逻辑处理，如果文件已经上传完成（完成分块合并操作），则不会进入到此方法中
     */
    handleUpload (fileInfo, taskRecord, option) {
      const { file } = fileInfo
      let lastUploadedSize = 0 // 上次断点续传时上传的总大小
      let uploadedSize = 0 // 已上传的大小
      const totalSize = file.size || 0 // 文件总大小
      const startMs = new Date().getTime() // 开始上传的时间
      const { exitPartList, CHUNK_SIZE, CHUNK_NUM, FILE_IDENTIFIER } = taskRecord

      // 获取从开始上传到现在的平均速度（byte/s）
      const getSpeed = () => {
        // 已上传的总大小 - 上次上传的总大小（断点续传）= 本次上传的总大小（byte）
        const intervalSize = uploadedSize - lastUploadedSize
        const nowMs = new Date().getTime()
        // 时间间隔（s）
        const intervalTime = (nowMs - startMs) / 1000
        return intervalSize / intervalTime
      }
      const uploadNext = async (partNumber) => {
        const start = Number(CHUNK_SIZE) * (partNumber - 1)
        const end = start + Number(CHUNK_SIZE)
        const blob = file.slice(start, end)
        const data = await getPreSignUrl({
          identifier: FILE_IDENTIFIER,
          partNumber
        })
        if (!data.code) {
          await axios.request({
            url: data,
            method: 'PUT',
            data: blob,
            headers: { 'Content-Type': 'application/octet-stream' }
          })
          return Promise.resolve({
            partNumber,
            uploadedSize: blob.size
          })
        } else {
          return Promise.reject(new Error(`分片${partNumber}， 获取上传地址失败`))
        }
      }
      /**
       * 更新上传进度
       * @param increment 为已上传的进度增加的字节量
       */
      const updateProcess = (increment) => {
        increment = Number(increment)
        // const factor = 1000 // 每次增加1000 byte
        // let from = 0
        // // 通过循环一点一点的增加进度
        // while (from <= increment) {
        //   from += factor
        //   uploadedSize += factor
        //   const percent = Math.round((uploadedSize / totalSize) * 100).toFixed(2)
        //   const percentage = Number(percent)
        // }
        uploadedSize += increment
        const percent = Math.round((uploadedSize / totalSize) * 100).toFixed(2)
        const percentage = Number(percent)

        const speed = getSpeed()
        const remainingTime =
          speed !== 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知'
        console.log('剩余大小：', (totalSize - uploadedSize) / 1024 / 1024, 'mb')
        console.log('当前速度：', (speed / 1024 / 1024).toFixed(2), 'mbps')
        console.log('预计完成：', remainingTime)

        if (this.isMultiFileMode) {
          if (this.fileUploadChunkQueue[fileInfo.file.uid]) {
            this.$store.commit('utils/updateUploadFileState', {
              id: fileInfo.file.uid,
              // 等待合并完成再设置上传完成
              uploadedSize,
              text: !isNaN(speed) && speed !== 0 ? `${this.$helper.formatFileSize(speed)}/s ${convertSeconds(Math.ceil((totalSize - uploadedSize) / speed))}` : '--',
              percentage: Math.min(99, percentage)
            })
          }
        } else {
          this.$store.commit('utils/setFileStateInfo', {
            visible: percentage < 100,
            text: '正在上传文件，请耐心等待',
            percentage: percentage
          })
        }
      }

      return new Promise((resolve) => {
        const failArr = []
        const queue = Queue(MAX_REQ_COUNT, {
          retry: RETRY,
          retryIsJump: false,
          workReject: function (reason, queue) {
            failArr.push(reason)
          },
          queueEnd: function (queue) {
            resolve({ failArr, fileInfo })
          }
        })
        this.fileUploadChunkQueue[file.uid] = queue
        for (let partNumber = 1; partNumber <= CHUNK_NUM; partNumber++) {
          const exitPart = (exitPartList || []).find(
            (exitPart) => exitPart.partNumber === partNumber
          )
          if (exitPart) {
            // 分片已上传完成，累计到上传完成的总额中,同时记录一下上次断点上传的大小，用于计算上传速度
            lastUploadedSize += Number(exitPart.size)
            updateProcess(exitPart.size)
          } else {
            queue.push(() =>
              uploadNext(partNumber).then((res) => {
                // 单片文件上传完成再更新上传进度
                updateProcess(res.uploadedSize)
              })
            )
          }
        }
        if (queue.getLength() === 0) {
          // 所有分片都上传完，但未合并，直接return出去，进行合并操作
          resolve({ failArr, fileInfo })
          return
        }
        queue.start()
      })
    },
    async handleHttpRequest (options) {
      const file = options.file
      const passed = this.checkFileType(file)
      if (!passed) return
      const fileInfo = { file, params: { ...this.extraParams } }
      this.fileCancelState[file.uid] = false
      this.startUploading(fileInfo)
      if (this.isMultiFileMode) {
        if (this.currentFiles.length < this.maxFileCount) {
          this.currentFiles.push(fileInfo)
        } else {
          this.uploadFileList.push(fileInfo)
          return
        }
      }
      this.$emit('update:loading', true)
      this.beginUpload(fileInfo)
    },
    /**
     * 移除文件列表中的文件
     * 如果文件存在上传队列任务对象，则停止该队列的任务
     */
    removeUploadingFile (fileId) {
      const queueObject = this.fileUploadChunkQueue[fileId]
      if (queueObject) {
        // 正在上传的文件
        queueObject.stop()
        this.fileUploadChunkQueue[fileId] = undefined
        this.fileCancelState[fileId] = undefined
      } else {
        const index = this.uploadFileList.findIndex(item => item.file.uid === fileId)
        if (index !== -1) {
          this.uploadFileList.splice(index, 1)
          this.fileCancelState[fileId] = undefined
        } else {
          // 在正在上传文件数组内，还没读取完文件
          const i = this.currentFiles.findIndex(item => item.file.uid === fileId)
          this.currentFiles.splice(i, 1)
          this.fileCancelState[fileId] = true
        }
      }
      this.$store.commit('utils/removeUploadFileById', fileId)
    },
    startUploading (fileInfo) {
      const { file } = fileInfo
      if (this.isMultiFileMode) {
        this.$store.commit('utils/updateUploadFileState', {
          id: file.uid,
          fileName: file.name,
          fileSize: file.size,
          fileType: this.fileType,
          text: '等待中...',
          canRetry: true,
          percentage: 0
        })
      } else {
        this.$store.commit('utils/setFileStateInfo', {
          visible: true,
          text: '正在读取文件中...',
          percentage: 0
        })
      }
    },
    async beginUpload (fileInfo) {
      if (this.isMultiFileMode) {
        this.$store.commit('utils/updateUploadFileState', {
          id: fileInfo.file.uid,
          text: '正在读取文件中......',
          percentage: 0
        })
      }
      const task = await this.initTaskAndSplitFile(fileInfo.file)
      if (task) {
        const { finished, path, taskRecord } = task
        const { FILE_IDENTIFIER: identifier } = taskRecord

        if (finished) {
          this.finishUploading(fileInfo, taskRecord)
          return path
        } else {
          const { failArr: errorList, fileInfo: errorFile } = await this.handleUpload(fileInfo, taskRecord)
          if (errorList.length > 0) {
            this.$message.error('部分分片上次失败，请尝试重新上传文件')
            if (this.isMultiFileMode) {
              this.errorFiles[errorFile.file.uid] = errorFile
              this.$store.commit('utils/updateUploadFileState', {
                id: fileInfo.file.uid,
                text: '发生错误',
                error: true
              })
            }
            return
          }
          const data = await mergeFile(identifier)
          this.finishUploading(fileInfo, data)
          return path
        }
      } else {
        if (!this.fileCancelState[fileInfo.file.uid]) {
          this.$message.error('获取上传任务失败')
          if (this.isMultiFileMode) {
            this.errorFiles[fileInfo.file.uid] = fileInfo
            this.$store.commit('utils/updateUploadFileState', {
              id: fileInfo.file.uid,
              text: '发生错误',
              error: true
            })
          } else {
            this.$store.commit('utils/setFileStateInfo', {
              visible: false
            })
          }
        } else {
          this.fileCancelState[fileInfo.file.uid] = undefined
        }
      }
    },
    finishUploading (fileInfo, data) {
      this.$emit('update:loading', false)
      this.fileCancelState[fileInfo.file.uid] = undefined
      if (this.isMultiFileMode) {
        this.$emit('change', data, fileInfo.params)
        this.$store.commit('utils/updateUploadFileState', {
          id: fileInfo.file.uid,
          text: '已完成',
          uploadedSize: null,
          percentage: 100
        })
        this.currentFiles.splice(this.currentFiles.indexOf(fileInfo), 1)
        if (this.uploadFileList.length) {
          const nextFile = this.uploadFileList.shift()
          this.currentFiles.push(nextFile)
          this.beginUpload(nextFile)
        } else {
          this.$emit('finish')
        }
      } else {
        this.$emit('change', data)
        this.$message.success('上传成功')
        this.fileName = fileInfo.file.name
        this.$store.commit('utils/setFileStateInfo', {
          visible: false
        })
      }
    },
    checkFileType (file) {
      // if (file.size / 1024 / 1024 / 1024 > 100) {
      //   this.$message.error('上传文件大小不能超过100G')
      //   return false
      // }
      if (!this.accept) return true
      const index = file.name.lastIndexOf('.')
      const suffix = file.name.slice(index + 1).toLowerCase()
      const suffixList = this.accept.split(',').map(s => s.trim().slice(1))
      for (const s of suffixList) {
        if (s === suffix) {
          return true
        }
      }
      if (this.accept === '.mp4, .mov') {
        this.$message.error('上传视频仅支持 mp4, mov 格式的视频文件')
      } else if (this.accept === '.tif') {
        this.$message.error('上传文件仅支持 tif 格式的文件')
      } else if (this.accept === '.zip') {
        this.$message.error('上传文件仅支持 zip 格式的文件')
      }
      return false
    },
    stopUploadFile ({ id }) {
      this.removeUploadingFile(id)
    },
    retryUploadFile ({ id }) {
      const fileInfo = this.errorFiles[id]
      if (this.currentFiles.length < this.maxFileCount) {
        this.currentFiles.push(fileInfo)
      } else {
        this.uploadFileList.push(fileInfo)
        return
      }
      this.beginUpload(fileInfo)
    }
  },
  created () {
    this.uploadFileList = []
    this.currentFiles = []
    this.errorFiles = {}
    this.fileCancelState = {}
    this.$root.$on('stopUploadFile', this.stopUploadFile)
    this.$root.$on('retryUploadFile', this.retryUploadFile)
  },
  beforeDestroy () {
    this.$root.$off('stopUploadFile', this.stopUploadFile)
    this.$root.$off('retryUploadFile', this.retryUploadFile)
  }
}
</script>
